<?php

namespace slinks\extensions\action\annotation;

use slinks\extensions\action\configuration\ConfigurationBuilder;
use slinks\extensions\action\configuration\ActionMapping;
use slinks\core\Config\Resource\FileResource;
use slinks\core\DependencyInjection\Definition;
use slinks\extensions\action\configuration\ResultMapping;
use slinks\core\DependencyInjection\ContainerBuilder;
use slinks\common\addendum\ReflectionAnnotatedClass;
use slinks\common\addendum\Addendum;
use slinks\core\dependencyInjectionExt\annotation\ServiceDefiningAnnotationUtil;
use slinks\common\io\IAnnotationVisitor;
use slinks\common\util\StringUtil;
use slinks\common\addendum\ReflectionAnnotatedMethod;

/**
 * Will build an array of IActionMappings based upon the annotations for a class.  
 * 
 * @author Navid Mitchell
 *
 */
class ActionDefiningAnnotationVisitor implements IAnnotationVisitor {

    private $container;
    private $configBuilder;

    const ACTION_TAG = 'slinks.action';

    public function __construct(ContainerBuilder $container, ConfigurationBuilder $configBuilder) {
        $this->container = $container;
        $this->configBuilder = $configBuilder;

        // these are included here so adendum will pick them up with thier short name when used as doc_block annotations.
        include_once 'Action.php';
        include_once 'Actions.php';
        include_once 'Results.php';
        include_once 'Result.php';
        include_once 'Route.php';
        
        // register the shortcut Annotation names with the Addendum
        Addendum::registerClass('Action', '\slinks\extensions\action\annotation\Action');
        Addendum::registerClass('Actions', '\slinks\extensions\action\annotation\Actions');
        Addendum::registerClass('Results', '\slinks\extensions\action\annotation\Results');
        Addendum::registerClass('Result', '\slinks\extensions\action\annotation\Result');
        Addendum::registerClass('Route', '\slinks\extensions\action\annotation\Route');
        
        // FIXME: this is the only way to make sure all annotation types are registered.
        ServiceDefiningAnnotationUtil::tryInit();
    }

    /**
     * This is called by the AnnotationReader when it reaches a new Class containing annotations in the filesytem.
     * @param string $classAnnotationInfo the ReflectionAnnotatedClass for the class containing the Annotations.
     * 		  This is part of the Addendum project http://code.google.com/p/addendum/
     */
    public function visit(ReflectionAnnotatedClass $classAnnotationInfo) {
        try {
            // see if there are any default result mappings
            $defaultResults = array();

            if ($classAnnotationInfo->hasAnnotation('Results')) {
                $defaultResults = $this->getResultMappings($classAnnotationInfo->getAnnotation('Results')->value);
            }

            $id = preg_replace('/\\\\/', '.', $classAnnotationInfo->getName());

            $foundActionMappings = $this->configureActionMappings($classAnnotationInfo, $id, $defaultResults);

            if ($foundActionMappings) {

                /* @var Definition $definition  */
                $definition = ServiceDefiningAnnotationUtil::createDefinition($classAnnotationInfo);

                $definition->addTag(self::ACTION_TAG);

                $this->container->addResource(new FileResource($classAnnotationInfo->getFileName()));

                // now add the service to the container
                $this->container->setDefinition($id, $definition);
            }
        } catch (\Exception $e) {
            throw new \Exception('Error processing class:' . $classAnnotationInfo->getName().' Message:('.$e->getMessage().')', $e->getCode(), $e);
        }
    }
    
    /**
     * Configures action mappings for the class.
     * If any wildcard action mappings are found then this will configure 1 mapping for each method that does not have its own mapping and is public.
     * @param ReflectionAnnotatedClass $class
     * @param type $serviceId
     * @param array $defaultResults
     * @return type 
     */
    private function configureActionMappings(ReflectionAnnotatedClass $class, $serviceId, array $defaultResults) {      
        $mappings = array();
       
        $classMappings = $this->getAllMappings($class, $serviceId,$defaultResults);
        
        // for any class mappings that have wildcard mappings then generate new mappings.
        // for each method in the class.
        foreach($classMappings as $mapping){
            if($mapping->getMethodName() == null){
                foreach($class->getMethods() as $method){
                   // generate wildcard mapping if qualified
                   if($this->shouldHaveWildcardMappingGenerated($method)){
                       $methodName = $method->getName();
                       /** @var $newMapping ActionMapping */
                       $newMapping = clone $mapping;
                       $newMapping->setMethodName($methodName);
                       $newMapping->setName($newMapping->getName().'_'.$methodName);
                       // now replace route wildcard parrameter with name.
                       // this is done so all routes are unique and can have urls generated for them.
                       $newMapping->getRoute()->setPattern(
                               preg_replace('/\{_actionMethod\}/',$methodName, $newMapping->getRoute()->getPattern()));

                       $mappings[] = $newMapping;
                   }
                }
            }else{
                $mappings[] = $mapping;
            }
        }
        
        
        // add any mappings for methods.
        // Any method mappings with the same name will overwrite class mappings. 
        // TODO: alert user if naming conflicts cause mappings to be overwritten.
        
        /* @var $method ReflectionAnnotatedMethod */
        foreach ($class->getMethods() as $method) {
            // must be public and not a getter or setter
            if($method->isPublic()){
               $mappings = array_merge($mappings,$this->getAllMappings($method, $serviceId, $defaultResults, $method->getName()));
            }
        }
        
       
        foreach($mappings as $mapping){
            $this->configBuilder->addActionMapping($mapping);
        }
        
        return count($mappings) > 0;
    }
    
    private function shouldHaveWildcardMappingGenerated(ReflectionAnnotatedMethod $method){
        $methodName = $method->getName();
        $ret = false;
        if(!StringUtil::beginsWith($methodName, 'get') && !StringUtil::beginsWith($methodName, 'set')){
            $ret = true;
        }
        return $ret;
    }
    
    /** 
     * 
     * @param ReflectionAnnotatedClass $obj
     * @param type $serviceId
     * @param array $defaultResults
     * @param type $actionMethod
     * @return array of ActionMapping objects
     *
     */
    private function getAllMappings($obj,$serviceId,array $defaultResults,$actionMethod=null){
        $ret = array();
        $class = null;
        
        if($obj instanceof ReflectionAnnotatedClass){
            $class = $obj;
        }else{
            $class = $obj->getDeclaringClass();
        }
        
        if($obj->hasAnnotation('Actions')) {
            $list = $obj->getAllAnnotations('Actions');
            foreach($list as $value){
                $ret = array_merge($ret,$this->getActionsMapping($class, $value, $serviceId, $actionMethod, $defaultResults));
            }
        }
        
        if($obj->hasAnnotation('Action')) {
            $list = $obj->getAllAnnotations('Action');
            foreach($list as $value){
                $ret[] = $this->getActionMapping($class, $value, $serviceId, $actionMethod, $defaultResults);
            }
        }
        
        return $ret;
    }
    
    /**
     * @return an array of ActionMappings for the given Actions annotation 
     */
    private function getActionsMapping(ReflectionAnnotatedClass $class,Actions $actions,$serviceId,$actionMethod,$defaultResults){
        $ret = array();
        if(!is_array($actions->value)){
            $actions->value = array($actions->value);
        }
        
        foreach($actions->value as $action){
            
            if(!($action instanceof Action)){
                throw new \InvalidArgumentException('The @Actions annotation must contain @Action annotations only. For Class:' . $class->getName());
            }
            
            $actionMapping = getActionMapping($class,
                                              $action,
                                              $serviceId,
                                              $actionMethod,
                                              array_merge($defaultResults, $this->getResultMappings($actions->results)),
                                              $actions->interceptorStack);
            $ret[] = $actionMapping; 
        }
        return ret;
    }
    
    /**
     * Creates a new ActionMapping if a name is not given a name will be auto generated. 
     * @return ActionMapping for a given Action annotation.
     */
    private function getActionMapping(ReflectionAnnotatedClass $class,
                                      Action $action,
                                      $serviceId,
                                      $actionMethod,
                                      $defaultResults,
                                      $defaultInterceptorStack = null){
        if($actionMethod == null){
            $actionMethod = $action->actionMethod;
        }
        // TODO: implement plugable default action naming 
        $ret = new ActionMapping(!empty($action->name) ? $action->name : $class->getShortName() . ($actionMethod == null ? '' : '_'.$actionMethod),
                                 $serviceId,
                                 $actionMethod,
                                 $this->getRoute($action, $class),
                                 new FileResource($class->getFileName()),
                                 array_merge($defaultResults, $this->getResultMappings($action->results)),
                                 $action->interceptorStack != null ? $action->interceptorStack : $defaultInterceptorStack );
        
        // validate ActionMapping;
        if($ret->getMethodName()==null){
            if(preg_match('/\{_actionMethod\}/', $ret->getRoute()->getPattern())==0){
                throw new \InvalidArgumentException('Wildcard @Action\'s must contain {_actionMethod} within the route pattern. For class: '. $class->getName());
            }
        }
        return $ret;
    }

    /**
     * @param Action $action the action to get the route for.
     * @return Route the route that is configured for this action.
     */
    private function getRoute(Action $action,ReflectionAnnotatedClass $class){
       if(empty($action->route)) {
           throw new \InvalidArgumentException('All @Action annotations must have a route defined. Missing for the Class:' . $class->getName());
       }
       $ret = null;
       if($action->route instanceof Route){
           /* @var $route Route */
           $route = $action->route;
           $ret = new \slinks\extensions\action\routing\Route($route->pattern,$route->defaults,$route->requirements,$route->options);
       }else{
           $ret = new \slinks\extensions\action\routing\Route($action->route);
       }
       return $ret;
    }
    
    /**
     * Return the result mappings for the given array.
     *
     * @param array $resultAnno
     */
    private function getResultMappings($resultAnno=null) {
        $ret = array();
        if ($resultAnno !== null) {

            if (is_array($resultAnno)) {
                /* @var $result Result */
                foreach ($resultAnno as $result) {
                    $ret[$result->name] = new ResultMapping($result->name, $result->type, $result->value);
                }
            } else {
                $ret[$resultAnno->name] = new ResultMapping($resultAnno->name, $resultAnno->type, $resultAnno->value);
            }
        }
        return $ret;
    }
   

}